import re
from datetime import timedelta
from typing import Dict, Optional, List

import pytest

from fixcore.cli.model import CLI
from tests.fixcore.db.entitydb import InMemoryDb
from fixcore.error import NoSuchTemplateError
from fixcore.model.graph_access import Section
from fixcore.query.model import Template, PathRoot
from fixcore.query.template_expander import render_template, TemplateExpanderBase, TemplateExpander
from fixcore.query.template_expander_service import TemplateExpanderService
from fixcore.types import Json
from fixcore.util import utc, from_utc


class InMemoryTemplateExpander(TemplateExpanderBase):
    def __init__(self) -> None:
        self.templates: Dict[str, Template] = {}
        self.props: Json = {}

    async def put_template(self, template: Template) -> None:
        self.templates[template.name] = template

    async def delete_template(self, name: str) -> None:
        self.templates.pop(name, None)

    async def get_template(self, name: str) -> Optional[Template]:
        return self.templates.get(name)

    async def list_templates(self) -> List[Template]:
        return list(self.templates.values())

    def default_props(self) -> Optional[Json]:
        return self.props

    async def parse_query_from_command_line(
        self, to_parse: str, on_section: str, env: Optional[Dict[str, str]] = None
    ) -> str:
        # straight forward implementation: simply strip the "search" command
        return re.sub("^search\\s+", "", to_parse)


@pytest.mark.asyncio
async def test_simple_expand(expander: InMemoryTemplateExpander) -> None:
    templates = [
        Template("foo", "Hey {{name}} - this is {{noun}}"),
        Template("bla", "One, two, {{t3}}"),
        Template("bar", "Heureka"),
    ]
    for t in templates:
        expander.templates[t.name] = t
    result, expands = await expander.expand(
        "Test: expand(foo, name=bart, noun=crazy). expand(bla, t3=jiffy). expand(bar)"
    )
    assert result == "Test: Hey bart - this is crazy. One, two, jiffy. Heureka"
    assert len(expands) == 3
    assert expands[0].template == "foo"
    assert expands[0].props == dict(name="bart", noun="crazy")
    assert expands[1].template == "bla"
    assert expands[1].props == dict(t3="jiffy")
    assert expands[2].template == "bar"
    assert expands[2].props == {}


@pytest.mark.asyncio
async def test_expand(expander: TemplateExpander) -> None:
    await expander.put_template(Template("albs", "is(aws_alb) and age>{{older_than}}"))
    result, expands = await expander.expand("query expand(albs, older_than=7d)")
    assert result == "query is(aws_alb) and age>7d"
    with pytest.raises(NoSuchTemplateError, match="does_not_exist"):
        await expander.expand("query expand(does_not_exist)")


@pytest.mark.asyncio
async def test_add_update_delete_get_list(expander: TemplateExpander) -> None:
    await expander.put_template(Template("albs", "is(aws_alb) and age>{{older_than}}"))
    result = await expander.get_template("albs")
    assert result and result.name == "albs" and result.template == "is(aws_alb) and age>{{older_than}}"
    assert len(await expander.list_templates()) == 1
    await expander.put_template(Template("albs", "is(aws_alb) and age>{{old}} limit 3"))
    assert (await expander.get_template("albs")).template == "is(aws_alb) and age>{{old}} limit 3"  # type: ignore
    await expander.delete_template("albs")
    assert len(await expander.list_templates()) == 0


@pytest.mark.asyncio
async def test_query_creation(cli: CLI) -> None:
    expander = TemplateExpanderService(InMemoryDb(Template, lambda k: k.name), cli)

    async def compare_query(query: str, on_section: str, omit_expansion: bool) -> None:
        direct = await expander.parse_query(query, on_section=on_section, omit_section_expansion=omit_expansion)
        search = await expander.parse_query(
            f"search {query}", on_section=on_section, omit_section_expansion=omit_expansion
        )
        sd = str(direct)
        ss = str(search)[0 : len(sd)]  # strip sort and limit generated by the command line parser
        assert sd == ss

    for section in [*Section.content_ordered, PathRoot]:
        await compare_query('is(aws_alb) and age>3d and mtime>"@UTC@"', section, True)
        await compare_query('is(aws_alb) and age>3d and mtime>"@UTC@"', section, False)


def test_render_simple() -> None:
    attrs = {"foo": "123", "list": ["a", "b", "c"]}
    res = render_template("query foo={{foo}} and test in {{list}}", attrs)
    assert res == 'query foo=123 and test in ["a", "b", "c"]'
    # fallback properties are used if the original list does not contain the value
    res2 = render_template("query foo={{foo}} and test in {{list}}", {}, [attrs])
    assert res2 == 'query foo=123 and test in ["a", "b", "c"]'


def test_render_list() -> None:
    attrs = {"is": ["alb", "elb"]}
    res = render_template("query {{#is.with_index}}{{^first}} or {{/first}}is({{value}}){{/is.with_index}}", attrs)
    assert res == "query is(alb) or is(elb)"


def test_render_as_list() -> None:
    # dictionary as list: every item will be available as [{key: ..., value: ...}]
    attrs: Json = {"props": {"foo": 1, "bla": 2, "bar": 3}}
    res = render_template(
        "query {{#props.as_list.with_index}}{{key}}=={{value}}{{^last}}, {{/last}}{{/props.as_list.with_index}}", attrs
    )
    assert res == "query foo==1, bla==2, bar==3"
    # if the input is already a list, the items will be available as is
    attrs = {"props": [{"key": "foo", "value": 1}, {"key": "bla", "value": 2}, {"key": "bar", "value": 3}]}
    res = render_template(
        "query {{#props.as_list.with_index}}{{key}}=={{value}}{{^last}}, {{/last}}{{/props.as_list.with_index}}", attrs
    )
    assert res == "query foo==1, bla==2, bar==3"
    # if the input is neither dict or list, it will be wrapped in a single element list
    attrs = {"props": "test"}
    res = render_template("query {{#props.as_list.with_index}}{{value}}=={{index}}{{/props.as_list.with_index}}", attrs)
    assert res == "query test==0"


def test_from_now() -> None:
    res = render_template("{{delta.from_now}}", {"delta": "4h"})
    in_4_hours = utc() + timedelta(hours=4)
    assert abs((in_4_hours - from_utc(res)).total_seconds()) < 1


def test_render_filter() -> None:
    attrs = {"foo": "123", "filter": 32}
    template = "query foo={{foo.parens}}{{#filter}} and some.other.prop == {{filter}}{{/filter}}"
    res = render_template(template, attrs)
    assert res == 'query foo="123" and some.other.prop == 32'
    attrs2 = {"foo": "123"}
    res = render_template(template, attrs2)
    assert res == 'query foo="123"'


def test_custom_tags() -> None:
    res = render_template("@test@ and @rest@", dict(test="work", rest="play"), tags=("@", "@"))
    assert res == "work and play"


@pytest.mark.asyncio
async def test_abbreviation(expander: TemplateExpander) -> None:
    async def with_section(section: Optional[str]) -> None:
        # name form
        assert str(await expander.parse_query("cloud=a", section)) == 'ancestors.cloud.reported.name == "a"'
        assert str(await expander.parse_query("account=a", section)) == 'ancestors.account.reported.name == "a"'
        assert str(await expander.parse_query("region=a", section)) == 'ancestors.region.reported.name == "a"'
        assert str(await expander.parse_query("zone=a", section)) == 'ancestors.zone.reported.name == "a"'
        # short form
        assert str(await expander.parse_query("cloud.name=a", section)) == 'ancestors.cloud.reported.name == "a"'
        assert str(await expander.parse_query("account.name=a", section)) == 'ancestors.account.reported.name == "a"'
        assert str(await expander.parse_query("region.name=a", section)) == 'ancestors.region.reported.name == "a"'
        assert str(await expander.parse_query("zone.name=a", section)) == 'ancestors.zone.reported.name == "a"'
        assert str(await expander.parse_query("usage.cpu.max > 3", section)) == "usage.cpu.max > 3"

    for s in Section.content_ordered + [None]:
        await with_section(s)
